package main;

/**
 * @author Ken "David" McClain
 * @author Zach Turk
 */

import java.awt.*;
import java.awt.event.*;
import java.nio.IntBuffer;
import java.util.ArrayList;

import javax.media.opengl.*;
import javax.media.opengl.awt.GLCanvas;
import javax.swing.*;

import p2p.Peer;
import physics.*;
import shape.*;

import com.jogamp.common.nio.Buffers;
import com.jogamp.opengl.util.FPSAnimator;

public class Main implements GLEventListener {
	private static final int TARGET_FPS = 30;
	
	private static JFrame appFrame;
	private static SceneGraphNode sceneGraphRoot;
	
	// Fields used for "Picked" functionality
	private static double left, right, top, bottom;
	private static ArrayList<PhysicsObject> objects;
	
	private static double height, width; // Screen height/width (in pixels). used by mouseDragged()
	
	// Scale of the root SceneGraphNode
	private static float SCALE = .15f;
	
	private static HalfSpace hsTop, hsBottom, hsLeft, hsRight;
	
	Vector2f mousePosition = new Vector2f(0, 0);
	
	PhysicsObject player, mace;
	float maceOrientation = 0;
	
	boolean gameOver = true, spinMace = false;
	
	// GUI labels
	private static JLabel statusBar, scoreBar, networkStatusBar;
	
	private static int score = 0, highScore = 0;
	
	private static Peer peer;
	
	private static boolean connectedToNetwork = false;
	
	public static void main(String[] args) {
		GLProfile.initSingleton();
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				(new Main()).createAndShowGUI();
			}
		});
		networkStatusBar = new JLabel("Network status: inactive");
		peer = new Peer(networkStatusBar);
		
	}
	
	private void createAndShowGUI() {
		// Fix for background flickering
		// System.setProperty("sun.awt.noerasebackground", "true");
		 
		GLProfile profile = GLProfile.getDefault();
		GLCapabilities capabilities = new GLCapabilities(profile);
		final GLCanvas canvas = new GLCanvas(capabilities);
		canvas.addGLEventListener(this);
		canvas.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				// Spin/swing
				if (e.getButton() == MouseEvent.BUTTON1 && !objects.contains(mace))
					attachObject(mace);
			}
			public void mouseReleased(MouseEvent e) {
				// Stop swinging
				//if (e.getButton() == MouseEvent.BUTTON1 && objects.contains(mace))
				//	detachObject(mace);
			}
		});
		
		canvas.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				
			}
			public void keyReleased(KeyEvent e) {
				// Reset game any time a key is pressed
				createNewGame();
			}
		});
		
		canvas.addMouseMotionListener(new MouseAdapter() {
			public void mouseDragged(MouseEvent e) {
				mouseMoved(e);
			}
			public void mouseMoved(MouseEvent e) {
				PhysicsObject obj = player;
				// Forced object positioning
				float x = getMouseX(e) - obj.centerOfMass.x;
				float y = getMouseY(e) - obj.centerOfMass.y;
				mousePosition = new Vector2f(x, y);
				/*
				if (x > left * 2 && x < right * 2)
					obj.position.x = x;
				if (y > bottom * 4 && y < top * 4)
					obj.position.y = y;
					*/
			}
			
		});
		
		canvas.addMouseWheelListener(new MouseWheelListener() {
			public void mouseWheelMoved(MouseWheelEvent e) {
				int notches = e.getWheelRotation();
				if (notches > 0) {
					// Scroll down
					SCALE /= ((float) notches / 5) + 1;
				} else {
					// Scroll up
					SCALE *= ((float) Math.abs(notches) / 5) + 1;
				}
				sceneGraphRoot.scale = SCALE;
			}
			
		});
		
		createNewGame();
		
		////////////////////////////////////////////
		// Top pane, for score board and game status
		JPanel paneTop = new JPanel();
		statusBar = new JLabel("Press any key to start a new game");
		scoreBar = new JLabel("Score: " + score + "    High Score: " + highScore + " ");
		statusBar.setForeground(Color.WHITE);
		scoreBar.setForeground(Color.WHITE);
		paneTop.setBackground(Color.BLACK);
		paneTop.add(scoreBar);
		paneTop.add(new JLabel("    "));
		paneTop.add(statusBar);
		
		////////////////////////////////////////
		// Bottom pane (P2P-related components)
		JPanel paneBottom = new JPanel(new FlowLayout(FlowLayout.LEFT));
		JRadioButton server = new JRadioButton("Start a Server", true),
					 client = new JRadioButton("Connect to a server");
		// P2P Server components (host name and start server button)
		final JPanel serverComp = new JPanel();
		serverComp.add(new JLabel("Port:"));
		final JTextField serverPort = new JTextField("1337", 6);
		serverComp.add(serverPort);
		final JButton serverStart = new JButton("Start server");
		serverStart.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (serverStart.getText().equals("Start server")) {
					serverStart.setText("Stop server");
					int port = 1337;
					try {
						port = Integer.parseInt(serverPort.getText());
					} catch (NumberFormatException nfe) {
						networkStatusBar.setText("Invalid port, defaulting to 1337");
						serverPort.setText("1337");
					}
					peer.createNetwork(port);
					connectedToNetwork = true;
				} else {
					peer.disconnectFromNetwork();
					connectedToNetwork = false;
					serverStart.setText("Start server");
				}
			}
		});
		serverComp.add(serverStart);
		
		// P2P Client components (host name, port, connect button)
		final JPanel clientComp = new JPanel();
		clientComp.add(new JLabel("Enter host/IP address:"));
		final JTextField clientHost = new JTextField("127.0.0.1", 8);
		clientComp.add(clientHost);
		clientComp.add(new JLabel("Port:"));
		final JTextField clientPort = new JTextField("1337", 6);
		clientComp.add(clientPort);
		final JButton clientConnect = new JButton("Connect");
		clientConnect.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if (clientConnect.getText().equals("Connect")) {
					clientConnect.setText("Disconnect");
					
					int port = 1337;
					try {
						port = Integer.parseInt(clientPort.getText());
					} catch (NumberFormatException nfe) {
						networkStatusBar.setText("Invalid port, defaulting to 1337");
						clientPort.setText("1337");
					}
					
					final int finalport = port;
					Thread t = new Thread() {
						{
							setDaemon(true);
						}
						public void run() {
							connectedToNetwork = peer.connectToNetwork(clientHost.getText(), finalport);
						}
					};
					t.start();
				} else {
					clientConnect.setText("Connect");
					peer.disconnectFromNetwork();
					connectedToNetwork = false;
				}
				
			}
		});
		clientComp.add(clientConnect);
		clientComp.setVisible(false);
		
		server.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				clientComp.setVisible(false);
				serverComp.setVisible(true);
				peer.disconnectFromNetwork();
				connectedToNetwork = false;
			}
		});
		client.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				clientComp.setVisible(true);
				serverComp.setVisible(false);
				peer.disconnectFromNetwork();
				connectedToNetwork = false;
			}
		});
		ButtonGroup bg = new ButtonGroup();
		bg.add(server);
		bg.add(client);
		paneBottom.add(server);
		paneBottom.add(client);
		paneBottom.add(serverComp);
		paneBottom.add(clientComp);
		paneBottom.add(networkStatusBar);
		
		appFrame = new JFrame("Project 1, Phase 3");
		appFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		appFrame.setMinimumSize(new Dimension(256, 256));
		appFrame.setLayout(new BorderLayout());
		appFrame.add(paneTop, BorderLayout.SOUTH);
		appFrame.add(canvas, BorderLayout.CENTER);
		appFrame.add(paneBottom, BorderLayout.NORTH);
		appFrame.pack();
		if (Toolkit.getDefaultToolkit().isFrameStateSupported(JFrame.MAXIMIZED_BOTH))
			appFrame.setExtendedState(appFrame.getExtendedState() | JFrame.MAXIMIZED_BOTH);
		appFrame.setVisible(true);
	}
	
	/** Reset the game board. */
	public void createNewGame() {
		gameOver = false;
		score = 0;
		objects = new ArrayList<PhysicsObject>();
		sceneGraphRoot = new SceneGraphNode(false);
		sceneGraphRoot.scale = SCALE;
		if (statusBar != null)
			statusBar.setText("Press any key to start a new game.");
		
		player = new Circle(.2f, 1f, 0f, 0f);
		player.inverseMass = 1f / Float.POSITIVE_INFINITY;
		player.inverseMomentOfInertia *= player.inverseMass;
		player.position.x = 3;
		player.position.y = -3;
		player.angularVelocity = 0f;
		player.acceleration.y = 0;
		attachObject(player);
		
		mace = new Triangle(1f, 1f, 0f, 0f);
		mace.inverseMass = 1f / Float.POSITIVE_INFINITY;
		mace.inverseMomentOfInertia *= mace.inverseMass;
		mace.position.x = 3;
		mace.position.y = -3;
		mace.angularVelocity = 0f;
		mace.acceleration.y = 0;
		
		PhysicsObject obj;
		for (int y = 0; y < 1; y++)
			for (int x = 0; x < 10; x++) {
				float mass = (float)(2 * Math.random() + .1);
				obj = new Triangle((float)(Math.sqrt(mass) * 1));
				obj.inverseMass = 1 / mass;
				obj.inverseMomentOfInertia *= obj.inverseMass;
				obj.position.x = -4.5f + x;
				obj.position.y = 2.5f - y;
				obj.velocity.x = (float)(1 * Math.random() - 0.5);
				obj.velocity.y = (float)(5 * Math.random() - 4.5);
				obj.angularVelocity = .5f;
				attachObject(obj);
			}
		
		
		/**** Half Spaces ****/
		hsLeft = new HalfSpace(new Vector2f(-5, 0), new Vector2f(1, 0));
		hsBottom = new HalfSpace(new Vector2f(0, -5), new Vector2f(0, 1));
		hsRight = new HalfSpace(new Vector2f(5, 0), new Vector2f(-1, 0));
		hsTop = new HalfSpace(new Vector2f(0, 5), new Vector2f(0, -1));
		attachObject(hsLeft);
		attachObject(hsBottom);
		attachObject(hsRight);
		attachObject(hsTop);
		
		// Draw halfspaces
		sceneGraphRoot.addChild(new SceneGraphNode(false) {
			/** Serial ID */
			private static final long serialVersionUID = -2893509264400566246L;

			public void renderGeometry(GLAutoDrawable drawable) {
				GL2 gl = drawable.getGL().getGL2();	
				gl.glColor3f(1, 1, 1);
				gl.glBegin(GL.GL_LINE_LOOP);
				gl.glVertex2f(-5, -5);
				gl.glVertex2f(5, -5);
				gl.glVertex2f(5, 5);
				gl.glVertex2f(-5, 5);
				gl.glEnd();
			}
		});
	}

	public static void attachObject(PhysicsObject object) {
		if (object.renderable != null)
			sceneGraphRoot.addChild(object.renderable);
		objects.add(object);
	}
	
	public void detachObject(PhysicsObject object) {
		if (object.renderable != null)
			sceneGraphRoot.removeChild(object.renderable);
		int index = objects.indexOf(object);
		if (index == -1)
			return; //throw new IllegalArgumentException();
		objects.set(index, objects.get(objects.size()-1));
		objects.remove(objects.size()-1);
	}
	
	// Convert mouse's X coordinate from pixels to JOGL's coordinate system.
	private float getMouseX(MouseEvent e) {
		float x = (float) (right - left)  * (e.getX() * 2 - (float) width) / ((float) (width));
		return x * (1 / SCALE) / 2;
	}
	
	// Convert mouse's Y coordinate from pixels to JOGL's coordinate system.
	private float getMouseY(MouseEvent e) {
		float y = (float) -(top - bottom) * (e.getY() * 2 - (float) height) / ((float) (height));
		return y * (1 / SCALE) / 2;
	}
	
	public void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		if (gameOver)
			// Draw red background if game is over
			gl.glClearColor(1, 0, 0, 0);
		else {
			// Draw clear background
			gl.glClearColor(0, 0, 0, 0);
			// Update score
			score++;
			if (score > highScore)
				highScore = score;
			scoreBar.setText("Score: " + score + "    High Score: " + highScore + " ");
		}
		
		gl.glClear(GL.GL_COLOR_BUFFER_BIT);
		
	    //for (PhysicsObject object : objects)
	    for (int i = 0; i < objects.size(); i++) {
			objects.get(i).updateState(1f / TARGET_FPS);
	    }
	    
		boolean noCollisions = false;
		for (int repeat = 0; repeat < 10 && !noCollisions; repeat++) {
			noCollisions = true;		
			for (int i = 0; i < objects.size(); i++) {
				PhysicsObject a = objects.get(i);
				for (int j = i + 1; j < objects.size(); j++) {
					PhysicsObject b = objects.get(j);
					if (a == null || b == null) 
						continue;
					CollisionInfo cInfo = a.getCollision(b);
					
					PhysicsObject other;
					if (cInfo != null) {
						if (a == player || b == player) {
							gameOver = true;
							statusBar.setText("Game over. Press any key to start a new game.");
							noCollisions = true;
							a.resolveCollision(b, cInfo);
							
						// Colliding with "top" half-space
						} else if (a == hsTop || b == hsTop) {
							// Get the object that collided with the top
							other = a;
							if (a == hsTop)
								other = b;
							
							// Send to network depending on X position
							float percent = (5 + other.position.x) / 10; // Percentage between far-left (0%) and far-right (100%)
							long size = Peer.ID_LIMIT - 3; // Ignoring us, our predecessor, and our successor.
							long someID = (int) (percent * size); // Get ID within the range.
							someID = (someID + peer.getID() + 2) % Peer.ID_LIMIT; // Add from our successor, wrapping around as needed
							
							// Check if we are connected, if not, bounce object back
							a.resolveCollision(b, cInfo);
							if (connectedToNetwork && peer.predecessor != null && peer.successor != null) {
								detachObject(other);
								other.position = new Vector2f(-other.position.x, other.position.y);
								other.velocity = new Vector2f(-other.velocity.x, other.velocity.y);
								peer.sendObject(other, someID);
							} else {
								noCollisions = false;
							}
							
						// Colliding with "left" half-space
						} else if (a == hsLeft || b == hsLeft) {
							// Determine the colliding object
							other = a;
							if (a == hsLeft)
								other = b;
							// Send object to successor
							a.resolveCollision(b, cInfo);
							if (connectedToNetwork && peer.successor != null) {
								detachObject(other);
								other.position = new Vector2f(-other.position.x, other.position.y);
								other.velocity = new Vector2f(-other.velocity.x, other.velocity.y);
								peer.sendObject(other, peer.successor.id);
							} else {
								noCollisions = false;
							}
							
						// Colliding with "right" half-space
						} else if (a == hsRight || b == hsRight) {
							// Determine the colliding object
							other = a;
							if (a == hsRight)
								other = b;
							// Send object to predecessor
							a.resolveCollision(b, cInfo);
							if (connectedToNetwork && peer.predecessor != null) {
								detachObject(other);
								other.position = new Vector2f(-other.position.x, other.position.y);
								other.velocity = new Vector2f(-other.velocity.x, other.velocity.y);
								peer.sendObject(other, peer.predecessor.id);
							} else {
								noCollisions = false;
							}
							
						// Colliding with "bottom" half-space, same as other collisions
						} else {
							noCollisions = false;
							a.resolveCollision(b, cInfo);
						}
						
					}
				}
			}
			
		}
		for (PhysicsObject object : objects)
			object.updateRenderable();
		
		// Move the player
		Vector2f dest = new Vector2f(mousePosition);
		dest.sumScale(player.position,  -1);
		if (dest.length() > 0.01) { //((Circle)player).radius) {
			player.velocity = dest;
		} else {
			player.velocity = new Vector2f(0, 0);
		}
		float r = ((Circle)player).radius;
		float maxScreen = 5;
		if (player.position.x > maxScreen - r) 
			player.position = new Vector2f(maxScreen - r, player.position.y);
		if (player.position.y > maxScreen - r) 
			player.position = new Vector2f(player.position.x, maxScreen - r);
		if (player.position.x < -maxScreen + r) 
			player.position = new Vector2f(-maxScreen + r, player.position.y);
		if (player.position.y < -maxScreen + r) 
			player.position = new Vector2f(player.position.x, -maxScreen + r);
		
		// Change mace orientation to point in direction of velocity?
		
		// Draw the mace
		if (objects.contains(mace)) {
			float x ,y;
			r = ((Circle)player).radius + (float) Math.sin(Math.PI / 12);
			x = player.position.x + (float) Math.sin(maceOrientation) * r;
			y = player.position.y + (float) Math.cos(maceOrientation) * r;
			mace.velocity = new Vector2f(mace.position);
			mace.position = new Vector2f(x, y);
			mace.velocity.sumScale(player.position, -1.5f);
			mace.orientation = -maceOrientation;
			if (maceOrientation > Math.PI * 2) { 
				detachObject(mace);
				maceOrientation = 0;
			} else {
				maceOrientation += 0.4;
			}
		}
		sceneGraphRoot.render(drawable);
		
	}
	
	public PhysicsObject getObjectFromNode(SceneGraphNode node) {
		for (PhysicsObject obj : objects) {
			if (obj.renderable == node) {
				return obj;
			}
		}
		return null;
	}
	
	public void dispose(GLAutoDrawable drawable) {
	}
	
	public void init(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		gl.glClearColor(0, 0, 0, 0);
		
		IntBuffer selectBuffer = Buffers.newDirectIntBuffer(3);
		gl.glSelectBuffer(selectBuffer.capacity(), selectBuffer);
		
		FPSAnimator fps = new FPSAnimator(drawable, TARGET_FPS);
		fps.start();
	}
	
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		final double UNIT = 2;
		
		if (width < height) {
			left = -UNIT / 2;
			right = UNIT / 2;
			top = (double)height / width * UNIT / 2;
			bottom = -top;
		} else {
			top = UNIT / 2;
			bottom = -UNIT / 2;
			right = (double)width / height * UNIT / 2;
			left = -right;
		}
		
		GL2 gl = drawable.getGL().getGL2();
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glOrtho(left, right, bottom, top, -1, 1);
		gl.glMatrixMode(GL2.GL_MODELVIEW);
		
		// Store height/width (in pixels!) for use in mouseDragged()
		Main.height = height;
		Main.width = width;
		
	}
}

